Option TryFrom 매크로 확장 계획 (매크로·제네릭, GlueSQL #1389)
NOTE
아직 착수 전 — 이번 주 일요일 작업 예정. 2024년에 열린 오래된 이슈라 재현 여부를 가장 먼저 확인해야 함(아래 “확인할 것” 참고). 셋 중 가장 개념적으로 흥미로운 케이스 — “당연히 될 것 같은” 제네릭 blanket impl이 왜 컴파일 에러가 나는지(오르판 규칙)를 직접 겪어보게 됨.
📌 개념
문제 — Value를 Option<T>로 바로 변환할 수 없음
let fee: Decimal = fee.try_into().unwrap(); // 됨
let fee: Option<Decimal> = fee.try_into().unwrap(); // 안 됨 — trait bound 에러이슈에 적힌 워크어라운드:
let fee: Option<Decimal> = match !fee.is_null() {
true => Some(Decimal::try_from(fee).unwrap()),
false => None,
};Value::Null이면 None, 아니면 Some(T)로 변환하는 게 자연스러운데 이걸 라이브러리가 대신 안 해줌.
원인 — 기존 매크로가 T만 만들고 Option<T>는 안 만듦
core/src/data/value/convert.rs에 이미 반복 패턴을 없애는 매크로가 있음:
// core/src/data/value/convert.rs:16-27
macro_rules! try_from_owned_value {
($($target:ty), *) => {$(
impl TryFrom<Value> for $target {
type Error = ValueError;
fn try_from(v: Value) -> Result<Self> {
Self::try_from(&v) // TryFrom<&Value>로 위임
}
}
)*}
}
try_from_owned_value!(
bool, i8, i16, i32, i64, i128, f32, f64, u8, u16, u32, u64, u128, usize, Decimal
);bool/i8/…/Decimal 각각에 대해 TryFrom<Value> for $target만 생성하고, TryFrom<Value> for Option<$target>은 아예 없음.
왜 “그냥 제네릭 blanket impl 하나”로는 안 되는가
가장 먼저 떠오르는 방법:
// 이렇게 하고 싶지만 컴파일 안 됨 (E0210)
impl<T> TryFrom<Value> for Option<T>
where
T: TryFrom<Value, Error = ValueError>,
{
type Error = ValueError;
fn try_from(v: Value) -> Result<Self> {
match v {
Value::Null => Ok(None),
v => T::try_from(v).map(Some),
}
}
}오르판 규칙(orphan rule)에 걸림. TryFrom도 Option도 이 크레이트가 정의한 게 아니라 둘 다 외부(std) 것이고, Self 타입인 Option<T> 안의 T가 제네릭 파라미터로 “가려지지 않은 채(uncovered)” 남아있음. 커버 규칙상 “로컬 타입(Value)이 처음 등장하기 전에 나오는 타입 파라미터는 전부 로컬 타입 안에 싸여 있어야” 하는데, Self = Option<T>가 먼저 나오고 T가 벌거벗은 채 있어서 위반. (Value가 TryFrom<Value>의 제네릭 인자로 로컬 타입이라 조건 일부는 만족하지만, Self 쪽 T가 안 가려진 게 문제.)
→ 실제로 이 코드를 타이핑해서 cargo check 에러 메시지(E0210: type parameter T must be covered by another type when it appears before the first local type)를 직접 보는 것 자체가 이번 작업의 학습 포인트.
고쳐야 하는 pseudocode — 매크로로 타입별 구체 impl 생성
제네릭 blanket impl 대신, 기존 매크로를 확장해서 매크로 전개 시점에 각 타입마다 구체적인(제네릭 아닌) impl을 여러 개 찍어냄. 이러면 매크로가 펼쳐진 뒤의 코드에는 impl<T> ...가 아니라 impl TryFrom<Value> for Option<Decimal>처럼 타입 파라미터가 없는 impl만 남아서 오르판 규칙에 안 걸림:
macro_rules! try_from_owned_value {
($($target:ty), *) => {$(
impl TryFrom<Value> for $target {
type Error = ValueError;
fn try_from(v: Value) -> Result<Self> {
Self::try_from(&v)
}
}
// 여기 추가
impl TryFrom<Value> for Option<$target> {
type Error = ValueError;
fn try_from(v: Value) -> Result<Self> {
match v {
Value::Null => Ok(None),
v => <$target>::try_from(v).map(Some),
}
}
}
)*}
}- 매크로 호출부(
try_from_owned_value!(bool, i8, ..., Decimal))는 그대로 두면, 이미 나열된 15개 타입 전부에 대해Option<T>impl이 한 번에 생김 TryFrom<&Value>(참조 버전)까지Option으로 확장할지는 이슈 범위 확인 필요 — 워크어라운드 코드가 소유(owned)Value만 다루고 있어서 우선 owned만 해도 이슈는 해결됨
확인할 것 (일요일 작업 전에)
- 이슈가 2024년(오래됨) 것이라, 지금
main에서도 정말 안 되는지부터 재현 확인 — REPL이나 작은 테스트로let x: Option<Decimal> = Value::Null.try_into()시도 Value::Null → None규칙이 이 코드베이스의 다른Option변환 관례와 일치하는지 (core/src/data/value.rs에 비슷한 패턴 있는지 검색)
테스트 방법
convert.rs안에 이미 있는TryFrom테스트들 근처(비슷한 이름의mod tests가 있는지 확인, 없으면 새로 추가)에 케이스 작성:assert_eq!(Option::<i64>::try_from(Value::I64(7)), Ok(Some(7))); assert_eq!(Option::<i64>::try_from(Value::Null), Ok(None)); assert!(Option::<i64>::try_from(Value::Str("x".into())).is_err());cargo test -p gluesql-core --lib data::value::convertcargo clippy -p gluesql-core --all-targets -- -D warnings,cargo fmt --all
배울 Rust 개념
- 매크로(
macro_rules!):$target:ty반복 패턴 안에 impl 블록을 두 개로 늘리는 것만으로 15개 타입 전부에 코드가 찍혀 나오는 걸 체감 - 트레잇과 제네릭 / 오르판 규칙: “제네릭이면 다 될 것 같다”는 직관이 깨지는 지점 — coherence(일관성) 규칙이 왜 필요한지, 매크로로 우회하는 게 왜 합법적인지
- 에러 처리:
Result<Self, ValueError>안에서match+.map(Some)으로Option을 감싸는 패턴
🔗 참고
- GlueSQL Issue #1389 — impl TryFrom
for common Option types - 관련 코드:
core/src/data/value/convert.rs(try_from_owned_value!매크로) - Rust Reference — 오르판 규칙(coherence)
관련 문서
- (Rust) 매크로(절차적 매크로 & macro_rules!) - 핵심 개념 및 특징 정리 —
try_from_owned_value!매크로 확장에 필요 - (Rust) 트레잇과 제네릭(Trait Object, Blanket Impl, 제네릭 경계) - 핵심 개념 및 특징 정리 — “오르판 규칙과 커버 규칙” 절에 이 이슈의 E0210 케이스를 그대로 정리해둠
-
- [Rust] Decimal 값 동등비교 누락 수정 계획(GlueSQL #1741) — 같은 날 후보로 같이 찾은 이슈
-
- [Rust] Glue::execute 배치 planning 수정 계획(GlueSQL #2009) — 같은 날 후보로 같이 찾은 이슈